package org.dragonfire.ti;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;

import org.dragonfire.config.ResourceConfig;
import org.dragonfire.util.DArray;
import org.dragonfire.util.token.TokenTable;

/**
 * @author Darth Android
 */
// TODO Add TI89(t)/92/92+/v200 support
// TODO TIFile.readFile(String filename)
// TODO TIFIle.readFile(InputStream file)
public class TIFile implements TIConstants {

	public static final int[] COMMENT_SIZE = new int[] {
			42, 42, 42, 42, 42, 42, 40, 40
	};
	public static final String[] FILE_EXTENSIONS = new String[] {
			"8xn", "8xl", "8xm", "8xy", "8xs", "8xp", "8xp", "8xi", "8xd", "8xy", "8xc", "8xl", "8xv", "", "", "", "",
			""
	};
	public static final int[] FOLDER_SIZE = new int[] {
			0, 0, 0, 0, 0, 0, 8, 8
	};
	public static final int[] HEADER_SIZE = new int[] {
			57, 57, 57, 57, 57
	};
	public static ResourceConfig lang = new ResourceConfig("org.dragonfire.ti.lang.tifile");
	private static final String[] MAGIC_KEYS = new String[] {
			"**TI73**" + (char) 26 + (char) 10 + (char) 0, "**TI82**" + (char) 26 + (char) 10 + (char) 0,
			"**TI83**" + (char) 26 + (char) 10 + (char) 0, "**TI83F*" + (char) 26 + (char) 10 + (char) 0,
			"**TI85**" + (char) 26 + (char) 12 + (char) 0, "**TI86**" + (char) 26 + (char) 10 + (char) 0,
			"**TI89**" + (char) 1 + (char) 0, "**TI92**" + (char) 1 + (char) 0
	};
	private static final int[] magicSize = new int[] {
			11, 11, 11, 11, 11, 11, 10, 10
	};
	private static final TokenTable[] tokenTables = new TokenTable[8];

	public static byte[] checkSum(byte[] data) {
		int check = 0;
		for (int i = 0; i < data.length; i++) {
			check += ((int) data[i]) & 0xFF;
		}
		byte[] sum = new byte[2];
		sum[1] = (byte) ((check & 0xFF00) >> 8);
		sum[0] = (byte) (check & 0xFF);
		return sum;
	}

	public static TokenTable getTokenTable(int calcType) {
		if (calcType > tokenTables.length)
			throw new ArrayIndexOutOfBoundsException("calcType is not a valid calculator type");
		if (tokenTables[calcType] == null) {
			TokenTable newTable = null;
			switch (calcType) {
				case CALC_8X:
					newTable = new TokenTable("<tokens/ti83p.tok>");
					break;
			}
			tokenTables[calcType] = newTable;
		}
		return tokenTables[calcType];
	}

	public static int getCalcVarType(int calcType, int varType) {
		switch (calcType) {
			case CALC_82:
				switch (varType) {
					case TYPE_REAL_NUMBER:
						return 0;
					case TYPE_LIST:
						return 1;
					case TYPE_MATRIX:
						return 2;
					case TYPE_EQUATION:
						return 3;
					case TYPE_PROGRAM:
						return 5;
					case TYPE_PROTECTED_PROGRAM:
						return 6;
					case TYPE_PICTURE:
						return 7;
					case TYPE_GRAPH_DATABASE:
						return 8;
					default:
						throw new IllegalArgumentException(lang.getString("ERROR_INVALID_VAR_TYPE", varType, calcType));
				}
			case CALC_83:
				switch (varType) {
					case TYPE_REAL_NUMBER:
						return 0;
					case TYPE_LIST:
						return 1;
					case TYPE_MATRIX:
						return 2;
					case TYPE_EQUATION:
						return 3;
					case TYPE_STRING:
						return 4;
					case TYPE_PROGRAM:
						return 5;
					case TYPE_PROTECTED_PROGRAM:
						return 6;
					case TYPE_PICTURE:
						return 7;
					case TYPE_GRAPH_DATABASE:
						return 8;
					case TYPE_NEW_EQUATION:
						return 11;
					case TYPE_COMPLEX_NUMBER:
						return 12;
					case TYPE_COMPLEX_LIST:
						return 13;
					default:
						throw new IllegalArgumentException(lang.getString("ERROR_INVALID_VAR_TYPE", varType, calcType));
				}
			case CALC_8X:
				switch (varType) {
					case TYPE_REAL_NUMBER:
						return 0;
					case TYPE_LIST:
						return 1;
					case TYPE_MATRIX:
						return 2;
					case TYPE_EQUATION:
						return 3;
					case TYPE_STRING:
						return 4;
					case TYPE_PROGRAM:
						return 5;
					case TYPE_PROTECTED_PROGRAM:
						return 6;
					case TYPE_PICTURE:
						return 7;
					case TYPE_GRAPH_DATABASE:
						return 8;
					case TYPE_NEW_EQUATION:
						return 11;
					case TYPE_COMPLEX_NUMBER:
						return 12;
					case TYPE_COMPLEX_LIST:
						return 13;
					case TYPE_APP_VAR:
						return 21;
					default:
						throw new IllegalArgumentException(lang.getString("ERROR_INVALID_VAR_TYPE", varType, calcType));
				}
			case CALC_85:
				switch (varType) {
					case TYPE_REAL_NUMBER:
						return 0;
					case TYPE_COMPLEX_NUMBER:
						return 1;
					case TYPE_VECTOR:
						return 2;
					case TYPE_COMPLEX_VECTOR:
						return 3;
					case TYPE_LIST:
						return 4;
					case TYPE_COMPLEX_LIST:
						return 5;
					case TYPE_MATRIX:
						return 6;
					case TYPE_COMPLEX_MATRIX:
						return 7;
					case TYPE_CONSTANT:
						return 8;
					case TYPE_COMPLEX_CONSTANT:
						return 9;
					case TYPE_EQUATION:
						return 10;
					case TYPE_STRING:
						return 12;
					case TYPE_GRAPH_DATABASE:
						return 13;
					case TYPE_PICTURE:
						return 17;
					case TYPE_PROGRAM:
						return 18;
					default:
						throw new IllegalArgumentException(lang.getString("ERROR_INVALID_VAR_TYPE", varType, calcType));
				}
			case CALC_86:
				switch (varType) {
					case TYPE_REAL_NUMBER:
						return 0;
					case TYPE_COMPLEX_NUMBER:
						return 1;
					case TYPE_VECTOR:
						return 2;
					case TYPE_COMPLEX_VECTOR:
						return 3;
					case TYPE_LIST:
						return 4;
					case TYPE_COMPLEX_LIST:
						return 5;
					case TYPE_MATRIX:
						return 6;
					case TYPE_COMPLEX_MATRIX:
						return 7;
					case TYPE_CONSTANT:
						return 8;
					case TYPE_COMPLEX_CONSTANT:
						return 9;
					case TYPE_EQUATION:
						return 10;
					case TYPE_STRING:
						return 12;
					case TYPE_GRAPH_DATABASE:
						return 13;
					case TYPE_PICTURE:
						return 17;
					case TYPE_PROGRAM:
						return 18;
					default:
						throw new IllegalArgumentException(lang.getString("ERROR_INVALID_VAR_TYPE", varType, calcType));
				}
			default:
				throw new IllegalArgumentException(lang.getString("ERROR_INVALID_VAR_TYPE", varType, calcType));
		}
	}

	public static VarEntry createProgramEntry(int calcType, Byte[] name, boolean locked, Byte[] data) {
		Byte[] newData = new Byte[data.length + 2];
		newData[0] = (byte) (data.length & 0xFF);
		newData[1] = (byte) ((data.length >> 8) & 0xFF);
		System.arraycopy(data, 0, newData, 2, data.length);
		VarEntry ve = new VarEntry(calcType, locked ? TYPE_PROTECTED_PROGRAM : TYPE_PROGRAM, name, newData);
		return ve;
	}

	public static void printReal(OutputStream os, int calcType, double value, Byte[] varName) {
		TIFile file = new TIFile(calcType, "Created by DASM");
		Byte[] data = new Byte[9];
		VarEntry ve = new VarEntry(calcType, TYPE_REAL_NUMBER, varName, data);
		file.addVarEntry(ve);
	}

	private int calcType = -1;
	private String comment = "";
	private String folder = "";
	private ArrayList<VarEntry> vars = new ArrayList<VarEntry>();

	public TIFile(int calculatorType) {
		this(calculatorType, "", "Created by DASM");
	}

	public TIFile(int calculatorType, String comment) {
		this(calculatorType, "", comment);
	}

	public TIFile(VarEntry singleEntry) {
		// create generic TIFile to hold this VarEntry
		this(singleEntry.getCalc());
		// add the VarEntry
		addVarEntry(singleEntry);
	}

	public TIFile(int calculatorType, String folder, String comment) {
		if (calculatorType > 5)
			throw new IllegalArgumentException("Invalid or unsupported calulator type '" + calculatorType + "'.");
		calcType = calculatorType;
		if (folder.length() > FOLDER_SIZE[calculatorType])
			throw new IllegalArgumentException("Folder name '" + folder + "' too long for this calculator.");
		this.folder = folder;
		if (comment.length() > COMMENT_SIZE[calculatorType])
			throw new IllegalArgumentException("Comment '" + comment + "' too long for this calculator.");
		this.comment = comment;
	}

	public TIFile(InputStream stream) {
		try {
			// discard magic that doesn't differientiate calculator type
			stream.read();
			stream.read();
			stream.read();
			stream.read();
			// determin calc type
			{
				char type1 = (char) stream.read();
				char type2 = (char) stream.read();
				char type3 = (char) stream.read();
				stream.read();// discard extra asterisk
				// discard extra magic
				stream.read();
				stream.read();
				// determin type by magic
				if (type1 == '9')
					calcType = CALC_92;
				else if (type1 == '7')
					calcType = CALC_73;
				else {
					switch (type2) {
						case '2':
							calcType = CALC_82;
							break;
						case '3':
							if (type3 == 'F')
								calcType = CALC_8X;
							else
								calcType = CALC_83;
							break;
						case '5':
							calcType = CALC_85;
							break;
						case '6':
							calcType = CALC_86;
							break;
						case '9':
							calcType = CALC_89;
						default:
							throw new Exception("Unknown calculator '" + type1 + type2 + type3 + "'"); // unknown
							// calc
							// type
					}
				}
				if (calcType != CALC_89 && calcType != CALC_92)
					stream.read(); // discard extra magic
			}
			// read in folder
			{
				StringBuffer sb = new StringBuffer(8);
				for (int i = 0; i < FOLDER_SIZE[getCalcType()]; i++) {
					char c = (char) stream.read();
					if (c > 0)
						sb.append(c);
				}
				folder = sb.toString();
			}
			// read in the comment
			{
				StringBuffer sb = new StringBuffer(42);
				for (int i = 0; i < COMMENT_SIZE[getCalcType()]; i++) {
					char c = (char) stream.read();
					if (c > 0)
						sb.append(c);
				}
				comment = sb.toString();
			}
			int dataSize = stream.read() + (stream.read() << 8);
			// read in variable entries
			if (getCalcType() == CALC_73 || getCalcType() == CALC_82 || getCalcType() == CALC_83
					|| getCalcType() == CALC_8X || getCalcType() == CALC_85 || getCalcType() == CALC_86) {
				int dataRead = 0;
				while (dataRead < dataSize) {
					// discard 2 bytes that are unused on most calcs and
					// repeated elsewhere on the other calcs
					stream.read();
					stream.read();
					int entryLength = stream.read() + (stream.read() << 8);
					int varType = stream.read();
					dataRead += 5;
					// read var name
					Byte[] name;
					{
						int nameLength = 8;
						if (getCalcType() == CALC_86 || getCalcType() == CALC_85) {
							nameLength = stream.read();
							dataRead++;
						}
						name = new Byte[nameLength];
						for (int i = 0; i < nameLength; i++) {
							name[i] = (byte) stream.read();
						}
						dataRead += nameLength;
					}
					boolean archived = false;
					int version = 0;
					if (getCalcType() == CALC_8X) {
						version = stream.read();
						archived = (stream.read() & 0x80) > 0;
						dataRead += 2;
					}
					// discard 2 bytes that duplicate the entryLength
					stream.read();
					stream.read();
					byte[] data = new byte[entryLength];
					stream.read(data);
					dataRead += 2 + entryLength;
					VarEntry ve = VarEntry.createEntry(getCalcType(), getCalcVarType(getCalcType(), varType), name,
							DArray.byteArray(data));
					if (getCalcType() == CALC_8X) {
						ve.setArchived(archived);
						ve.setVersion(version);
					}
					addVarEntry(ve);
				}
			}
			else if (getCalcType() == CALC_89 || getCalcType() == CALC_92) {
				throw new Exception("Feature not supported");
				// TODO add support
			}
		} catch (Exception e) {
			throw new RuntimeException("Invalid file format", e);
		}
	}

	public void addVarEntry(VarEntry ve) {
		getVars().add(ve);
	}

	public byte[] buildFile() {
		byte[] file = new byte[size()];
		System.arraycopy(MAGIC_KEYS[calcType].getBytes(), 0, file, 0, 11);
		System.arraycopy(comment.getBytes(), 0, file, 11, comment.getBytes().length);
		// get the data from all the var entries
		byte[] data = data();
		int index = MAGIC_KEYS[calcType].length() + COMMENT_SIZE[calcType];
		// next two bytes are always the data length
		file[index++] = (byte) (data.length & 0xFF);
		file[index++] = (byte) ((data.length >> 8) & 0xFF);
		System.arraycopy(data, 0, file, index, data.length);
		System.arraycopy(checkSum(data), 0, file, file.length - 2, 2);
		return file;
	}

	private int calcByMagic(String magic) {
		if (magic.length() != 3)
			return -1;
		for (int i = 0; i < MAGIC_KEYS.length; i++)
			if (MAGIC_KEYS[i].substring(4, 7).equals(magic))
				return i;
		return -1;
	}

	public byte[] data() {
		byte[] data = new byte[dataSize()];
		int index = 0;
		for (int i = 0; i < vars.size(); i++) {
			byte[] var = vars.get(i).build();
			System.arraycopy(var, 0, data, index, var.length);
			index += var.length;
		}
		return data;
	}

	public int dataSize() {
		int size = 0;
		for (int i = 0; i < vars.size(); i++) {
			size += vars.get(i).size();
		}
		return size;
	}

	public ArrayList<VarEntry> getVars() {
		return vars;
	}

	/**
	 * @return the comment
	 */
	public String getComment() {
		return comment;
	}

	/**
	 * @param comment
	 *            the comment to set
	 */
	public void setComment(String comment) {
		if (comment == null)
			comment = "";
		if (comment.length() > COMMENT_SIZE[getCalcType()])
			comment = comment.substring(0, COMMENT_SIZE[getCalcType()]);
		this.comment = comment;
	}

	/**
	 * @return the folder
	 */
	public String getFolder() {
		return folder;
	}

	/**
	 * @param folder
	 *            the folder to set
	 */
	public void setFolder(String folder) {
		if (folder == null)
			folder = "";
		this.folder = folder;
	}

	/**
	 * Returns the total size of this file. This is calculated from the header
	 * size plus the sum of the size of all data sections
	 * 
	 * @return
	 */
	public int size() {
		return dataSize() + HEADER_SIZE[calcType];
	}

	public void writeFile(OutputStream os) throws IOException {
		os.write(buildFile());
	}

	public void writeFile(String name) throws FileNotFoundException, IOException {
		FileOutputStream fos = new FileOutputStream(name);
		writeFile(fos);
	}

	/**
	 * @return the calcType
	 */
	public int getCalcType() {
		return calcType;
	}
}
